在项目中需要用到一个随机数,将其作为唯一且无法重复,第一个想到的就是MongoDB里面的objectID,将其作为一个唯一且不重复的键值。

ObjectId是一个12字节的 BSON 类型字符串。按照字节顺序,一次代表:

  • 4字节:UNIX时间戳
  • 3字节:表示运行MongoDB的机器
  • 2字节:表示生成此_id的进程
  • 3字节:由一个随机数开始的计数器生成的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// machineId stores machine id generated once and used in subsequent calls
// to NewObjectId function.
var machineId = readMachineId()
var processId = os.Getpid()
// objectIdCounter is atomically incremented when generating a new ObjectId
// using NewObjectId() function. It's used as a counter part of an id.
var objectIdCounter = readRandomUint32()

// readRandomUint32 returns a random objectIdCounter.
func readRandomUint32() uint32 {
var b [4]byte
_, err := io.ReadFull(rand.Reader, b[:])
if err != nil {
panic(fmt.Errorf("cannot read random object id: %v", err))
}
return uint32((uint32(b[0]) << 0) | (uint32(b[1]) << 8) | (uint32(b[2]) << 16) | (uint32(b[3]) << 24))
}

// readMachineId generates and returns a machine id.
// If this function fails to get the hostname it will cause a runtime error.
func readMachineId() []byte {
var sum [3]byte
id := sum[:]
hostname, err1 := os.Hostname()
if err1 != nil {
_, err2 := io.ReadFull(rand.Reader, id)
if err2 != nil {
panic(fmt.Errorf("cannot get hostname: %v; %v", err1, err2))
}
return id
}
hw := md5.New()
hw.Write([]byte(hostname))
copy(id, hw.Sum(nil))
return id
}

// NewObjectId returns a new unique ObjectId.
func NewObjectId() ObjectId {
var b [12]byte
// Timestamp, 4 bytes, big endian
binary.BigEndian.PutUint32(b[:], uint32(time.Now().Unix()))
// Machine, first 3 bytes of md5(hostname)
b[4] = machineId[0]
b[5] = machineId[1]
b[6] = machineId[2]
// Pid, 2 bytes, specs don't specify endianness, but we use big endian.
b[7] = byte(processId >> 8)
b[8] = byte(processId)
// Increment, 3 bytes, big endian
i := atomic.AddUint32(&objectIdCounter, 1)
b[9] = byte(i >> 16)
b[10] = byte(i >> 8)
b[11] = byte(i)
return ObjectId(b[:])
}

MongoDB objectID改变

在golang的MongoDB官方库中,objectID的生成方式发生了改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// "go.mongodb.org/mongo-driver/bson/primitive"

var objectIDCounter = readRandomUint32()
var processUnique = processUniqueBytes()

// NewObjectID generates a new ObjectID.
func NewObjectID() ObjectID {
var b [12]byte

binary.BigEndian.PutUint32(b[0:4], uint32(time.Now().Unix()))
copy(b[4:9], processUnique[:])
putUint24(b[9:12], atomic.AddUint32(&objectIDCounter, 1))

return b
}

可以看到这12字节,已改为,4字节unix时间戳+5字节随机值+随机值加上1。

查看官方文档也可以发现,其对objectID的定义已经更改。

  • a 4-byte value representing the seconds since the Unix epoch,
  • a 5-byte random value, and
  • a 3-byte counter, starting with a random value.

只找到一篇博客对这个修改的推测,我也确实没有找到确切的修改理由,我认为博主说的有些道理。

引用自:Mongo ObjectId 早就不用机器标识和进程号了

mongo 的 C++ 源码中,设置 ObjectId 中间 5 个字节的函数叫 setInstanceUnique,而在官方 golang 驱动中叫 processUnique,字面意思相近,都是说明这个值的作用是“区分不同进程实例”,而这个值具体怎么实现并没有什么要求,所以,使用“机器标识+进程号”来拿区分不同进程实例是可以的,使用互无关联的随机数来拿区分不同进程实例也是可以的。

可想而知,“在同一秒内,两个进程实例产生了相同的 5 字节随机数,且刚巧这时候两个进程的自增计数器的值也是相同的”——这种情况发生的概率实在太低了,完全可以认为不可能发生,所以使用互无关联的随机数来拿区分不同进程实例是完全合乎需求的。

那问题来了,为什么不继续使用“机器标识+进程号”呢?主观臆测开始。

问题就在于,机器标识和进程号一定就那么可靠吗,尤其在这个物理机鲜见,虚拟机、云主机、容器横行的时代?

先说机器标识码,ObjectId 的机器标识码是取系统 hostname 哈希值的前几位,问题来了,想必在座的各位都有干过吧:准备了几台虚拟机,hostname 都是默认的 localhost,谁都想着这玩意儿能有什么用,还得刻意给不同机器起不同的 hostname?此外,hostname 在容器、云主机里一般默认就是随机数,也不会检查同一集群里是否有 hostname 重名。

再说进程号,这个问题就更大了,要知道,容器内的进程拥有自己独立的进程空间,在这个空间里只用它自己这一个进程(以及它的子进程),所以它的进程号永远都是 1。也就是说,如果某个服务(既可以是 mongo 实例也可以是 mongo 客户端)是使用容器部署的,无论部署多少个实例,在这个服务上生成的 ObjectId,第八第九个字节恒为 0000 0001,相当于说这两个字节废了。

综上,与其使用一个固定值来“区分不同进程实例”,且这个固定值还是人类随意设置或随机生成的 hostname 加上一个可能恒为 1 的进程号,倒不如每次都随机生成一个新值。